package trading.algo;

import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import trading.algo.APIConnectivityTask.APIConnectMasterTask;
import trading.algo.BearTask.AfterMarketHourTask;
import trading.algo.BearTask.BeforeMarketHourTask;
import trading.algo.BearTask.MarketHourTask;
import trading.algo.BearToBullTask.CleanPositionTask;
import trading.algo.BullTask.AfterMarketHourBullTask;
import trading.algo.BullTask.BeforeMarketHourBullTask;
import trading.algo.BullTask.MarketHourBullTask;
import trading.algo.BullTask.NewSTKShortOrdersTask;
import trading.algo.BullToBearTask.PositionCleanUpTask;
import trading.algo.BullToBearTask.ProtectionOrderCreationMasterTask;
import trading.algo.BullToBearTask.VIXSecondFrontMonthFuturePriceTask;
import trading.bo.DBAccessorBO;
import trading.bo.EmailAccessorBO;
import trading.bo.IBAccessorBO;
import trading.dataentry.MonitoredStocks;
import trading.util.Constants;
import trading.util.MarketTimeHelper;
import trading.util.OrderSplitHelper;

/**
 * This class is the key to the whole project. It does the following things: 1.
 * in bull market: 1.1 determine market hours 1.2 in market hour, check stock
 * price every 5 seconds 1.3 check if bull market turns to bear 1.4 update
 * historical low price 1.5 create orders when certain criteria is met 1.6
 * sanity check if all positions are well protected 2. in bear market: 2.1
 * determine market hours 2.2 in market hour, check stock price every 5 seconds
 * 2.3 check if bear market turns to bull 2.4 after market hour, check if the
 * market condition meets criteria to create new orders 2.5 before market hour,
 * sync all db tables, in case a reverse split or split happens 3. bull market
 * turns to bear market 3.1 sanity check all position is cleared. 3.2 create
 * option order for protection. 4. bear market turns to bull market 4.1 clean up
 * all positions 4.2 create first bull market orders
 * 
 * This class is like a state machine. It goes from 1->3->2->4->1
 * 
 * @author ntantai
 * 
 */
public class MasterAlgo {

    private State currentState = null;
    private ScheduledThreadPoolExecutor beforeMarketHourThreadScheduler;
    private ScheduledThreadPoolExecutor marketHourThreadScheduler;
    private ScheduledThreadPoolExecutor afterMarketHourThreadScheduler;
    private DBAccessorBO dbAccessorBO;

    public void setDbAccessorBO(DBAccessorBO dbAccessorBO) {
        this.dbAccessorBO = dbAccessorBO;
    }

    private IBAccessorBO ibAccessorBO;

    public void setIbAccessorBO(IBAccessorBO ibAccessorBO) {
        this.ibAccessorBO = ibAccessorBO;
    }

    private MarketTimeHelper marketTimeHelper;

    public void setMarketTimeHelper(MarketTimeHelper marketTimeHelper) {
        this.marketTimeHelper = marketTimeHelper;
    }

    private EmailAccessorBO emailAccessorBO;

    public void setEmailAccessorBO(EmailAccessorBO emailAccessorBO) {
        this.emailAccessorBO = emailAccessorBO;
    }

    private OrderSplitHelper orderSplitHelper;

    public void setOrderSplitHelper(OrderSplitHelper orderSplitHelper) {
        this.orderSplitHelper = orderSplitHelper;
    }

    public enum State {
        Bull, Bear, Bull_To_Bear, Bear_To_Bull;
    }

    public void fromBearToBearToBull() {
        this.currentState = State.Bear_To_Bull;
        cancelCurrentThreads();
        process();
        this.emailAccessorBO.sendMktSentimentChangeEmail("BEAR", "BULL");
    }

    public void fromBullToBullToBear() {
        this.currentState = State.Bull_To_Bear;
        cancelCurrentThreads();
        process();
        this.emailAccessorBO.sendMktSentimentChangeEmail("BULL", "BEAR");
    }

    private void cancelCurrentThreads() {
        try {
            this.marketHourThreadScheduler.shutdown();
            this.marketHourThreadScheduler.awaitTermination(30, TimeUnit.SECONDS);
            this.marketHourThreadScheduler.shutdownNow();
            
            this.afterMarketHourThreadScheduler.shutdown();
            this.afterMarketHourThreadScheduler.awaitTermination(30, TimeUnit.SECONDS);
            this.afterMarketHourThreadScheduler.shutdownNow();
            
            this.beforeMarketHourThreadScheduler.shutdown();
            this.beforeMarketHourThreadScheduler.awaitTermination(30, TimeUnit.SECONDS);
            this.beforeMarketHourThreadScheduler.shutdownNow();
        } catch (Exception e) {

        }
    }

    public void process() {
        cancelCurrentThreads();
        if (currentState == null) {
            currentState = State
                    .valueOf(this.dbAccessorBO.getMarketSentiment());
        }
        if (currentState == State.Bear) {
            processBear();
        } else if (currentState == State.Bear_To_Bull) {
            processBearToBull();
        } else if (currentState == State.Bull) {
            processBull();
        } else if (currentState == State.Bull_To_Bear) {
            processBullToBear();
        }
    }

    private void processBear() {
        Calendar nextMarketOpeningTime = this.marketTimeHelper
                .getNextMarketOpeningTime();
        // before market hour
        beforeMarketHourThreadScheduler = new ScheduledThreadPoolExecutor(Constants.Algo.CORE_PULL_SIZE);
        Calendar beforeMarketHourTaskTime = (Calendar) nextMarketOpeningTime
                .clone();
        beforeMarketHourTaskTime.add(Calendar.MINUTE, -30);
        long delay = this.marketTimeHelper.getDelay(beforeMarketHourTaskTime);
        BeforeMarketHourTask beforeMarketHourTask = new BeforeMarketHourTask(
                ibAccessorBO, dbAccessorBO);
        beforeMarketHourThreadScheduler.schedule(beforeMarketHourTask, delay, TimeUnit.MILLISECONDS);

        Calendar beforeMarketHourTaskFinishTime = (Calendar) beforeMarketHourTaskTime
                .clone();
        beforeMarketHourTaskFinishTime.add(Calendar.MINUTE, 5);
        delay = this.marketTimeHelper.getDelay(beforeMarketHourTaskFinishTime);
        ThreadPoolShutdownTask threadKillerTask = new ThreadPoolShutdownTask(
                this.beforeMarketHourThreadScheduler);
        beforeMarketHourThreadScheduler.schedule(threadKillerTask, delay, TimeUnit.MILLISECONDS);

        marketHourThreadScheduler = new ScheduledThreadPoolExecutor(Constants.Algo.CORE_PULL_SIZE);
        Calendar connectTime = (Calendar) nextMarketOpeningTime.clone();
        connectTime.add(Calendar.SECOND, -40);
        delay = this.marketTimeHelper.getDelay(connectTime);
        APIConnectMasterTask apiConnectMasterTask = new APIConnectMasterTask(
                ibAccessorBO);
        marketHourThreadScheduler.schedule(apiConnectMasterTask, delay, TimeUnit.MILLISECONDS);

        // during market hour
        PriceUpdateTask priceUpdateTask1 = new PriceUpdateTask(
                this.ibAccessorBO, MonitoredStocks.TICKERS.UVXY.name());
        PriceUpdateTask priceUpdateTask2 = new PriceUpdateTask(
                this.ibAccessorBO, MonitoredStocks.TICKERS.VXX.name());
        MarketHourTask marketHourTask = new MarketHourTask(this.dbAccessorBO,
                this, this.emailAccessorBO);

        Calendar priceUpdateTask1StartTime = (Calendar) nextMarketOpeningTime
                .clone();
        priceUpdateTask1StartTime.add(Calendar.SECOND, 5);

        Calendar priceUpdateTask2StartTime = (Calendar) nextMarketOpeningTime
                .clone();
        priceUpdateTask2StartTime.add(Calendar.SECOND, 8);

        Calendar marketHourTaskStartTime = (Calendar) nextMarketOpeningTime
                .clone();
        marketHourTaskStartTime.add(Calendar.SECOND, 9);

        delay = this.marketTimeHelper.getDelay(priceUpdateTask1StartTime);
        marketHourThreadScheduler.schedule(priceUpdateTask1, delay, TimeUnit.MILLISECONDS);
        delay = this.marketTimeHelper.getDelay(priceUpdateTask2StartTime);
        marketHourThreadScheduler.schedule(priceUpdateTask2, delay, TimeUnit.MILLISECONDS);
        delay = this.marketTimeHelper.getDelay(marketHourTaskStartTime);
        marketHourThreadScheduler.scheduleAtFixedRate(marketHourTask, delay, 5000, TimeUnit.MILLISECONDS);

        // after market hour
        Calendar nextClosingTime = this.marketTimeHelper
                .getNextMarketClosingTime();
        ThreadPoolShutdownTask threadKillerTask1 = new ThreadPoolShutdownTask(this.marketHourThreadScheduler);
        delay = this.marketTimeHelper.getDelay(nextClosingTime);
        marketHourThreadScheduler.schedule(threadKillerTask1, delay, TimeUnit.MILLISECONDS);

        Calendar stopPriceUpdateTime = (Calendar) nextClosingTime.clone();
        stopPriceUpdateTime.add(Calendar.SECOND, -5);
        StopPriceUpdateTask stopPriceUpdateTask = new StopPriceUpdateTask(
                ibAccessorBO, emailAccessorBO);
        delay = this.marketTimeHelper.getDelay(stopPriceUpdateTime);
        marketHourThreadScheduler.schedule(stopPriceUpdateTask, delay, TimeUnit.MILLISECONDS);

        Calendar marketHourClosingTaskTime = this.marketTimeHelper
                .getAfterMarketHourTaskTime();

        AfterMarketHourTask afterMarketHourTask = new AfterMarketHourTask(this,
                ibAccessorBO, dbAccessorBO, emailAccessorBO, marketTimeHelper,
                orderSplitHelper);

        afterMarketHourThreadScheduler = new ScheduledThreadPoolExecutor(Constants.Algo.CORE_PULL_SIZE);
        delay = this.marketTimeHelper.getDelay(marketHourClosingTaskTime);
        afterMarketHourThreadScheduler.schedule(afterMarketHourTask, delay, TimeUnit.MILLISECONDS);

        Calendar afterMarketHourThreadSchedulerKillTime = (Calendar) marketHourClosingTaskTime
                .clone();
        afterMarketHourThreadSchedulerKillTime.add(Calendar.MINUTE, 5);
        delay = this.marketTimeHelper.getDelay(afterMarketHourThreadSchedulerKillTime);
        ThreadPoolShutdownTask threadKillerTask2 = new ThreadPoolShutdownTask(
                this.afterMarketHourThreadScheduler);
        afterMarketHourThreadScheduler.schedule(threadKillerTask2, delay, TimeUnit.MILLISECONDS);
    }

    /**
     * This happens during market hour close all positions then create new
     * orders
     */
    private void processBearToBull() {
        ScheduledThreadPoolExecutor threadScheduler = new ScheduledThreadPoolExecutor(Constants.Algo.CORE_PULL_SIZE);
        int taskID = 0;
        // Step 1: get open orders
        OpenOrdersTask openOrderTask = new OpenOrdersTask(this.ibAccessorBO,
                this.dbAccessorBO);
        threadScheduler.schedule(openOrderTask, taskID, TimeUnit.SECONDS);
        taskID += 2;

        // Step 2: update portfolio positions
        threadScheduler.schedule(new UpdateAccountValueTask(ibAccessorBO, dbAccessorBO, true),
                taskID, TimeUnit.SECONDS);
        taskID += 5;

        // Step 3: close all open orders
        CancelOrderMasterTask cancelOrderTask = new CancelOrderMasterTask(
                ibAccessorBO, dbAccessorBO, emailAccessorBO);
        threadScheduler.schedule(cancelOrderTask, taskID, TimeUnit.SECONDS);
        taskID += 15;

        // Step 4: create mkt order for all open positions
        CleanPositionTask cleanPositionTask = new CleanPositionTask(
                ibAccessorBO, dbAccessorBO, emailAccessorBO, orderSplitHelper);
        threadScheduler.schedule(cleanPositionTask, taskID, TimeUnit.SECONDS);
        taskID += 660;

        // Step 5: change market sentiment to BULL and write to db
        // this step doesn't need to be a threadScheduler task
        this.currentState = State.Bull;
        this.dbAccessorBO.updateMarketSentiment(this.currentState.name());

        // Step 6: clean up VIX price table and VIX contango table
        this.dbAccessorBO.cleanUpVIXSpot();
        this.dbAccessorBO.cleanUpVIXContango();

        // Step 7: update historical low
        MonitoredStocks UVXYStat = this.dbAccessorBO
                .getMonitoredStockData(MonitoredStocks.TICKERS.UVXY.name());
        String date = this.marketTimeHelper.getSimpleDateFormatter("yyyyMMdd")
                .format(new Date());
        this.dbAccessorBO.updateHistoricalLowPriceAndDate(
                MonitoredStocks.TICKERS.UVXY.name(), UVXYStat.getLastPrice()
                        + "", date);

        // Step 8: create initial orders for bull market
        taskID += 10; // to wait for previous order execution finish.
        double trailAmount = UVXYStat.getLastPrice()
                * Constants.Algo.BULL_SHORT_PROTECTION_PERCENTAGE / 100.0;
        NewSTKShortOrdersTask shortOrderTask = new NewSTKShortOrdersTask(
                ibAccessorBO, dbAccessorBO, emailAccessorBO, trailAmount,
                UVXYStat.getLastPrice() + trailAmount, orderSplitHelper);
        threadScheduler.schedule(shortOrderTask, taskID, TimeUnit.SECONDS);
        taskID += 25;

        // Step 9: update open order table
        OpenOrdersTask openOrderTask1 = new OpenOrdersTask(this.ibAccessorBO,
                this.dbAccessorBO);
        threadScheduler.schedule(openOrderTask1, taskID, TimeUnit.SECONDS);
        taskID += 2;

        // Step 10: stop updating price
        StopPriceUpdateTask stopPriceUpdateTask = new StopPriceUpdateTask(
                ibAccessorBO, emailAccessorBO);
        threadScheduler.schedule(stopPriceUpdateTask, taskID, TimeUnit.SECONDS);
        taskID += 2;

        // Step 11: start master algo processing after market hour
        Calendar mktClosingTime = this.marketTimeHelper
                .getNextMarketClosingTime();
        mktClosingTime.add(Calendar.HOUR, 6);
        long delay = this.marketTimeHelper.getDelay(mktClosingTime);
        threadScheduler.schedule(new MasterAlgoStartUpTask(this), delay, TimeUnit.MILLISECONDS);

        // Step 12: kill threadScheduler
        Calendar killThreadSchedulerTime = (Calendar) mktClosingTime.clone();
        killThreadSchedulerTime.add(Calendar.SECOND, 30);
        delay = this.marketTimeHelper.getDelay(killThreadSchedulerTime);
        ThreadPoolShutdownTask threadKillerTask = new ThreadPoolShutdownTask(threadScheduler);
        threadScheduler.schedule(threadKillerTask, delay, TimeUnit.MILLISECONDS);
    }

    private void processBull() {
        Calendar nextMarketOpeningTime = this.marketTimeHelper
                .getNextMarketOpeningTime();
        // before market hour
        beforeMarketHourThreadScheduler = new ScheduledThreadPoolExecutor(Constants.Algo.CORE_PULL_SIZE);
        Calendar beforeMktHrTaskTime = (Calendar) nextMarketOpeningTime.clone();
        beforeMktHrTaskTime.add(Calendar.MINUTE, -30);
        long delay = this.marketTimeHelper.getDelay(beforeMktHrTaskTime);
        BeforeMarketHourBullTask beforeMktHrBullTask = new BeforeMarketHourBullTask(
                ibAccessorBO, dbAccessorBO, emailAccessorBO);
        beforeMarketHourThreadScheduler.schedule(beforeMktHrBullTask, delay, TimeUnit.MILLISECONDS);

        Calendar beforeMktHrTaskFinishTime = (Calendar) beforeMktHrTaskTime
                .clone();
        beforeMktHrTaskFinishTime.add(Calendar.MINUTE, 5);
        delay = this.marketTimeHelper.getDelay(beforeMktHrTaskFinishTime);
        ThreadPoolShutdownTask threadKillerTask = new ThreadPoolShutdownTask(
                beforeMarketHourThreadScheduler);
        beforeMarketHourThreadScheduler.schedule(threadKillerTask, delay, TimeUnit.MILLISECONDS);

        marketHourThreadScheduler = new ScheduledThreadPoolExecutor(Constants.Algo.CORE_PULL_SIZE);
        Calendar connectTime = (Calendar) nextMarketOpeningTime.clone();
        connectTime.add(Calendar.SECOND, -40);
        delay = this.marketTimeHelper.getDelay(connectTime);
        APIConnectMasterTask apiConnectMasterTask = new APIConnectMasterTask(
                ibAccessorBO);
        marketHourThreadScheduler.schedule(apiConnectMasterTask, delay, TimeUnit.MILLISECONDS);
        // during market hour
        PriceUpdateTask priceUpdateTask1 = new PriceUpdateTask(
                this.ibAccessorBO, MonitoredStocks.TICKERS.UVXY.name());
        PriceUpdateTask priceUpdateTask2 = new PriceUpdateTask(
                this.ibAccessorBO, MonitoredStocks.TICKERS.VXX.name());
        MarketHourBullTask marketHourTask = new MarketHourBullTask(
                ibAccessorBO, this.dbAccessorBO, this, marketTimeHelper,
                this.emailAccessorBO, orderSplitHelper);

        Calendar priceUpdateTask1StartTime = (Calendar) nextMarketOpeningTime
                .clone();
        priceUpdateTask1StartTime.add(Calendar.SECOND, 5);

        Calendar priceUpdateTask2StartTime = (Calendar) nextMarketOpeningTime
                .clone();
        priceUpdateTask2StartTime.add(Calendar.SECOND, 8);

        Calendar marketHourTaskStartTime = (Calendar) nextMarketOpeningTime
                .clone();
        marketHourTaskStartTime.add(Calendar.SECOND, 9);

        delay = this.marketTimeHelper.getDelay(priceUpdateTask1StartTime);
        marketHourThreadScheduler.schedule(priceUpdateTask1, delay, TimeUnit.MILLISECONDS);
        delay = this.marketTimeHelper.getDelay(priceUpdateTask2StartTime);
        marketHourThreadScheduler.schedule(priceUpdateTask2, delay, TimeUnit.MILLISECONDS);
        delay = this.marketTimeHelper.getDelay(marketHourTaskStartTime);
        marketHourThreadScheduler.scheduleAtFixedRate(marketHourTask, delay, 5000, TimeUnit.MILLISECONDS);

        // after market hour
        Calendar nextClosingTime = this.marketTimeHelper
                .getNextMarketClosingTime();
        Calendar stopPriceUpdateTaskTime = (Calendar) nextClosingTime.clone();
        stopPriceUpdateTaskTime.add(Calendar.SECOND, -5);
        delay = this.marketTimeHelper.getDelay(stopPriceUpdateTaskTime);
        StopPriceUpdateTask stopPriceUpdateTask = new StopPriceUpdateTask(
                ibAccessorBO, emailAccessorBO);
        marketHourThreadScheduler.schedule(stopPriceUpdateTask, delay, TimeUnit.MILLISECONDS);

        delay = this.marketTimeHelper.getDelay(nextClosingTime);
        ThreadPoolShutdownTask threadKillerTask1 = new ThreadPoolShutdownTask(
                this.marketHourThreadScheduler);
        marketHourThreadScheduler.schedule(threadKillerTask1, delay, TimeUnit.MILLISECONDS);

        Calendar marketHourClosingTaskTime = this.marketTimeHelper
                .getAfterMarketHourTaskTime();

        delay = this.marketTimeHelper.getDelay(marketHourClosingTaskTime);
        AfterMarketHourBullTask afterMarketHourTask = new AfterMarketHourBullTask(
                ibAccessorBO, dbAccessorBO, emailAccessorBO, this);

        afterMarketHourThreadScheduler = new ScheduledThreadPoolExecutor(Constants.Algo.CORE_PULL_SIZE);
        afterMarketHourThreadScheduler.schedule(afterMarketHourTask, delay, TimeUnit.MILLISECONDS);

        Calendar afterMarketHourThreadSchedulerKillTime = (Calendar) marketHourClosingTaskTime
                .clone();
        afterMarketHourThreadSchedulerKillTime.add(Calendar.MINUTE, 5);
        delay = this.marketTimeHelper.getDelay(afterMarketHourThreadSchedulerKillTime);
        ThreadPoolShutdownTask threadKillerTask2 = new ThreadPoolShutdownTask(
                this.afterMarketHourThreadScheduler);
        afterMarketHourThreadScheduler.schedule(threadKillerTask2, delay, TimeUnit.MILLISECONDS);
    }

    private void processBullToBear() {
        ScheduledThreadPoolExecutor threadScheduler = new ScheduledThreadPoolExecutor(Constants.Algo.CORE_PULL_SIZE);
        int taskID = 0;
        // Step 1: make sure all positions are cleaned up
        // wait for 15 seconds to give cushion time for protection order
        // execution
        taskID += 15;
        // update open orders
        OpenOrdersTask openOrderTask = new OpenOrdersTask(this.ibAccessorBO,
                this.dbAccessorBO);
        threadScheduler.schedule(openOrderTask, taskID, TimeUnit.SECONDS);
        taskID += 2;

        // update portfolio positions
        threadScheduler.schedule(new UpdateAccountValueTask(ibAccessorBO, dbAccessorBO, true),
                taskID, TimeUnit.SECONDS);
        taskID += 10;

        // clean up all position and orders, just in case something goes wrong
        // with protection orders
        PositionCleanUpTask positionCleanUpTask = new PositionCleanUpTask(
                ibAccessorBO, dbAccessorBO, emailAccessorBO, orderSplitHelper);
        threadScheduler.schedule(positionCleanUpTask, taskID, TimeUnit.SECONDS);
        taskID += 660;
        // Step 2: create protection option orders
        VIXSecondFrontMonthFuturePriceTask vixSecondFrontMonthFuturePriceTask = new VIXSecondFrontMonthFuturePriceTask(
                ibAccessorBO, marketTimeHelper);
        threadScheduler.schedule(vixSecondFrontMonthFuturePriceTask, taskID, TimeUnit.SECONDS);
        taskID += 9;

        ProtectionOrderCreationMasterTask protectionOrderCreationTask = new ProtectionOrderCreationMasterTask(
                ibAccessorBO, dbAccessorBO, emailAccessorBO, marketTimeHelper);
        threadScheduler.schedule(protectionOrderCreationTask, taskID, TimeUnit.SECONDS);
        taskID += 15;
        //
        // Step 3: update open orders table
        OpenOrdersTask openOrderTask1 = new OpenOrdersTask(this.ibAccessorBO,
                this.dbAccessorBO);
        threadScheduler.schedule(openOrderTask1, taskID, TimeUnit.SECONDS);
        taskID += 2;

        // Step 4: stop updating price
        StopPriceUpdateTask stopPriceUpdateTask = new StopPriceUpdateTask(
                ibAccessorBO, emailAccessorBO);
        threadScheduler.schedule(stopPriceUpdateTask, taskID, TimeUnit.SECONDS);
        taskID += 2;

        // Step 5: change market sentiment
        this.currentState = State.Bear;
        this.dbAccessorBO.updateMarketSentiment(this.currentState.name());

        // Step 6: start master algo processing after market hour
        Calendar mktClosingTime = this.marketTimeHelper
                .getNextMarketClosingTime();
        mktClosingTime.add(Calendar.HOUR, 1);
        long delay = this.marketTimeHelper.getDelay(mktClosingTime);
        threadScheduler.schedule(new MasterAlgoStartUpTask(this), delay, TimeUnit.MILLISECONDS);

        // Step 7: kill the threadScheduler
        Calendar killThreadSchedulerTime = (Calendar) mktClosingTime.clone();
        killThreadSchedulerTime.add(Calendar.MINUTE, 30);
        delay = this.marketTimeHelper.getDelay(killThreadSchedulerTime);
        ThreadPoolShutdownTask threadKillerTask = new ThreadPoolShutdownTask(threadScheduler);
        threadScheduler.schedule(threadKillerTask, delay, TimeUnit.MILLISECONDS);
    }
}
